/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hethongviet.vending.bluetooth;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;

import com.hethongviet.vending.Constant;
import com.hethongviet.vending.AppConfig;
import com.hethongviet.vending.bluetooth.BluetoothConstants;
import com.hethongviet.vending.bluetooth.OnBluetoothListener;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for incoming
 * connections, a thread for connecting with a device, and a thread for
 * performing data transmissions when connected.
 */
public class BluetoothService
{

	// Debugging
	private static final String TAG = "BluetoothChatService";
	private static final boolean isDebug = AppConfig.IS_DEBUG;

	// Name for the SDP record when creating server socket
	private static final String NAME = "Vending";

	// Unique UUID for this application
	private static final UUID MY_UUID = UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");

	// Member fields
	private final BluetoothAdapter mAdapter;
	private AcceptThread mAcceptThread;
	private ConnectThread mConnectThread;
	private ConnectedThread mConnectedThread;
	private int mState;

	private String mMessageBluetoothIncoming = "";
	private boolean mIsStartGetMessageBluetooth = false;

	/** Must be a Context object */
	private OnBluetoothListener mOnBluetoothListener = null;

	private Handler mHandler = new Handler()
	{
		@Override
		public void handleMessage(Message msg)
		{
			if (mOnBluetoothListener == null)
			{
				if (isDebug)
				{
					Log.d(TAG, "null bluetooth listener!!!!!!!!!!!!");
				}
				return;
			}

			switch (msg.what)
			{
				case BluetoothConstants.MESSAGE_STATE_CHANGE:
					Log.d(TAG, "BLUETOOTH_MESSAGE_STATE_CHANGE: " + msg.arg1);
					switch (msg.arg1)
					{
						case BluetoothConstants.STATE_CONNECTED:

							if (AppConfig.IS_PRINT_BLUETOOTH_LOG)
								Toast.makeText((Context) mOnBluetoothListener,
										"Connected bluetooth",
										Toast.LENGTH_SHORT).show();

							break;

						case BluetoothConstants.STATE_CONNECTING:
							if (AppConfig.IS_PRINT_BLUETOOTH_LOG)
								Toast.makeText((Context) mOnBluetoothListener,
										"Connecting bluetooth",
										Toast.LENGTH_SHORT).show();
							break;

						case BluetoothConstants.STATE_LISTEN:
						case BluetoothConstants.STATE_NONE:
							if (AppConfig.IS_PRINT_BLUETOOTH_LOG)
								Toast.makeText((Context) mOnBluetoothListener,
										"Not Connect bluetooth",
										Toast.LENGTH_SHORT).show();

							break;
					}
					break;
				case BluetoothConstants.MESSAGE_WRITE:
					byte[] writeBuf = (byte[]) msg.obj;
					// construct a string from the buffer
					String writeMessage = new String(writeBuf);
					Log.d(TAG, "Send message = " + writeMessage);

					break;
				case BluetoothConstants.MESSAGE_READ:
					String readMessage = (String) msg.obj;
					Log.d(TAG, "Bluetooth message : " + readMessage);
					Log.d(TAG, "isStartGetMessage : "
							+ mIsStartGetMessageBluetooth);
					if (readMessage != null && !readMessage.equals("")
							&& !mIsStartGetMessageBluetooth)
					{
						Log.d(TAG, "---start!");
						mIsStartGetMessageBluetooth = true;
						mMessageBluetoothIncoming = "";
						new CountDownTimer(
								Constant.MAX_TIME_OUT_BLUETOOTH_MESSAGE,
								Constant.MAX_TIME_OUT_BLUETOOTH_MESSAGE)
						{

							@Override
							public void onTick(long millisUntilFinished)
							{

							}

							@Override
							public void onFinish()
							{
								Log.d(TAG, "---end wait!");
								mOnBluetoothListener.onBluetoothInComingMessage(mMessageBluetoothIncoming);
								mMessageBluetoothIncoming = "";
								mIsStartGetMessageBluetooth = false;
							}
						}.start();
					}

					mMessageBluetoothIncoming += readMessage;

					break;
				case BluetoothConstants.MESSAGE_DEVICE_NAME:
					break;
				case BluetoothConstants.MESSAGE_TOAST:
					if (AppConfig.IS_PRINT_BLUETOOTH_LOG)
						Toast.makeText(
								(Context) mOnBluetoothListener,
								msg.getData().getString(
										BluetoothConstants.TOAST),
								Toast.LENGTH_SHORT).show();
					break;
			}
		}
	};

	/**
	 * Constructor. Prepares a new BluetoothChat session.
	 * 
	 * @param context
	 *            The UI Activity Context
	 * @param handler
	 *            A Handler to send messages back to the UI Activity
	 */
	private BluetoothService()
	{
		mAdapter = BluetoothAdapter.getDefaultAdapter();
		mState = BluetoothConstants.STATE_NONE;

		// If the adapter is null, then Bluetooth is not supported
		if (mAdapter == null)
		{
			if (AppConfig.IS_DEBUG)
			{
				Log.d(TAG, "***** Bluetooth is not available!");
				Toast.makeText((Context) mOnBluetoothListener,
						"***** Bluetooth is not available!", Toast.LENGTH_LONG).show();
			}
		}
	}

	public static BluetoothService instance = null;

	public synchronized static BluetoothService getInstance()
	{
		if (instance == null)
		{
			instance = new BluetoothService();
		}
		return instance;
	}

	/**
	 * Set the current state of the chat connection
	 * 
	 * @param state
	 *            An integer defining the current connection state
	 */
	private synchronized void setState(int state)
	{
		if (isDebug)
		{
			Log.d(TAG, "setState() " + mState + " -> " + state);
		}
		mState = state;

		// Give the new state to the Handler so the UI Activity can update
		mHandler.obtainMessage(BluetoothConstants.MESSAGE_STATE_CHANGE, state,
				-1).sendToTarget();
	}

	public void prepareAndConnect(String address)
	{
		// Only if the state is STATE_NONE, do we know that we haven't
		// started already
		if (getState() == BluetoothConstants.STATE_NONE)
		{
			// Start the Bluetooth service
			start();
		}
		if (getState() == BluetoothConstants.STATE_CONNECTED)
			return;

		try
		{
			// Get the BLuetoothDevice object
			BluetoothDevice device = mAdapter.getRemoteDevice(address);
			// Attempt to connect to the device
			connect(device);
		} catch (Exception e)
		{
			e.printStackTrace();
			stop();
		}

	}

	/**
	 * Return the current connection state.
	 */
	public synchronized int getState()
	{
		return mState;
	}

	/**
	 * Start the chat service. Specifically start AcceptThread to begin a
	 * session in listening (server) mode. Called by the Activity onResume()
	 */
	private synchronized void start()
	{
		if (isDebug)
			Log.d(TAG, "start");

		// Cancel any thread attempting to make a connection
		if (mConnectThread != null)
		{
			mConnectThread.cancel();
			mConnectThread = null;
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null)
		{
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Start the thread to listen on a BluetoothServerSocket
		if (mAcceptThread == null)
		{
			mAcceptThread = new AcceptThread();
			mAcceptThread.start();
		}
		setState(BluetoothConstants.STATE_LISTEN);
	}

	/**
	 * Start the ConnectThread to initiate a connection to a remote device.
	 * 
	 * @param device
	 *            The BluetoothDevice to connect
	 */
	public synchronized void connect(BluetoothDevice device)
	{
		if (isDebug)
			Log.d(TAG, "connect to: " + device);

		// Cancel any thread attempting to make a connection
		if (mState == BluetoothConstants.STATE_CONNECTING)
		{
			if (mConnectThread != null)
			{
				mConnectThread.cancel();
				mConnectThread = null;
			}
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null)
		{
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Start the thread to connect with the given device
		mConnectThread = new ConnectThread(device);
		mConnectThread.start();
		setState(BluetoothConstants.STATE_CONNECTING);
	}

	/**
	 * Start the ConnectedThread to begin managing a Bluetooth connection
	 * 
	 * @param socket
	 *            The BluetoothSocket on which the connection was made
	 * @param device
	 *            The BluetoothDevice that has been connected
	 */
	public synchronized void connected(BluetoothSocket socket,
			BluetoothDevice device)
	{
		if (isDebug)
			Log.d(TAG, "connected");

		// Cancel the thread that completed the connection
		if (mConnectThread != null)
		{
			mConnectThread.cancel();
			mConnectThread = null;
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null)
		{
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Cancel the accept thread because we only want to connect to one
		// device
		if (mAcceptThread != null)
		{
			mAcceptThread.cancel();
			mAcceptThread = null;
		}

		// Start the thread to manage the connection and perform transmissions
		mConnectedThread = new ConnectedThread(socket);
		mConnectedThread.start();

		// Send the name of the connected device back to the UI Activity
		Message msg = mHandler.obtainMessage(BluetoothConstants.MESSAGE_DEVICE_NAME);
		Bundle bundle = new Bundle();
		bundle.putString(BluetoothConstants.DEVICE_NAME, device.getName());
		msg.setData(bundle);
		mHandler.sendMessage(msg);

		setState(BluetoothConstants.STATE_CONNECTED);
	}

	/**
	 * Stop all threads
	 */
	public synchronized void stop()
	{
		if (isDebug)
			Log.d(TAG, "stop");
		if (mConnectThread != null)
		{
			mConnectThread.cancel();
			mConnectThread = null;
		}
		if (mConnectedThread != null)
		{
			mConnectedThread.cancel();
			mConnectedThread = null;
		}
		if (mAcceptThread != null)
		{
			mAcceptThread.cancel();
			mAcceptThread = null;
		}
		setState(BluetoothConstants.STATE_NONE);
	}

	/**
	 * Write to the ConnectedThread in an unsynchronized manner
	 * 
	 * @param out
	 *            The bytes to write
	 * @see ConnectedThread#write(byte[])
	 */
	public void write(byte[] out)
	{
		// Create temporary object
		ConnectedThread r;
		// Synchronize a copy of the ConnectedThread
		synchronized (this)
		{
			if (mState != BluetoothConstants.STATE_CONNECTED)
				return;
			r = mConnectedThread;
		}
		// Perform the write unsynchronized
		r.write(out);
	}

	/**
	 * Indicate that the connection attempt failed and notify the UI Activity.
	 */
	private void connectionFailed()
	{

		// Send a failure message back to the Activity
		Message msg = mHandler.obtainMessage(BluetoothConstants.MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(BluetoothConstants.TOAST, "Unable to connect device");
		msg.setData(bundle);
		mHandler.sendMessage(msg);
		start();
	}

	/**
	 * Indicate that the connection was lost and notify the UI Activity.
	 */
	private void connectionLost()
	{

		// Send a failure message back to the Activity
		Message msg = mHandler.obtainMessage(BluetoothConstants.MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(BluetoothConstants.TOAST, "Device connection was lost");
		msg.setData(bundle);
		mHandler.sendMessage(msg);
		start();
	}

	public OnBluetoothListener getOnBluetoothListener()
	{
		return mOnBluetoothListener;
	}

	public void setOnBluetoothListener(OnBluetoothListener onBluetoothListener)
	{
		this.mOnBluetoothListener = onBluetoothListener;
	}

	/**
	 * This thread runs while listening for incoming connections. It behaves
	 * like a server-side client. It runs until a connection is accepted (or
	 * until cancelled).
	 */
	private class AcceptThread extends Thread
	{
		// The local server socket
		private final BluetoothServerSocket mmServerSocket;

		public AcceptThread()
		{
			BluetoothServerSocket tmp = null;

			// Create a new listening server socket
			try
			{
				tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
			} catch (Exception e)
			{
				Log.e(TAG, "listen() failed", e);
			}
			mmServerSocket = tmp;
		}

		public void run()
		{
			if (isDebug)
				Log.d(TAG, "BEGIN mAcceptThread" + this);
			setName("AcceptThread");
			BluetoothSocket socket = null;

			// Listen to the server socket if we're not connected
			while (mState != BluetoothConstants.STATE_CONNECTED)
			{
				try
				{
					// This is a blocking call and will only return on a
					// successful connection or an exception
					socket = mmServerSocket.accept();
				} catch (Exception e)
				{
					Log.e(TAG, "accept() failed", e);
					break;
				}

				// If a connection was accepted
				if (socket != null)
				{
					synchronized (BluetoothService.this)
					{
						switch (mState)
						{
							case BluetoothConstants.STATE_LISTEN:
							case BluetoothConstants.STATE_CONNECTING:
								// Situation normal. Start the connected thread.
								connected(socket, socket.getRemoteDevice());
								break;
							case BluetoothConstants.STATE_NONE:
							case BluetoothConstants.STATE_CONNECTED:
								// Either not ready or already connected.
								// Terminate new socket.
								try
								{
									socket.close();
								} catch (Exception e)
								{
									Log.e(TAG,
											"Could not close unwanted socket",
											e);
								}
								break;
						}
					}
				}
			}
			if (isDebug)
				Log.i(TAG, "END mAcceptThread");
		}

		public void cancel()
		{
			if (isDebug)
				Log.d(TAG, "cancel " + this);
			try
			{
				mmServerSocket.close();
			} catch (Exception e)
			{
				Log.e(TAG, "close() of server failed", e);
			}
		}
	}

	/**
	 * This thread runs while attempting to make an outgoing connection with a
	 * device. It runs straight through; the connection either succeeds or
	 * fails.
	 */
	private class ConnectThread extends Thread
	{
		private final BluetoothSocket mmSocket;
		private final BluetoothDevice mmDevice;

		public ConnectThread(BluetoothDevice device)
		{
			mmDevice = device;
			BluetoothSocket tmp = null;

			// Get a BluetoothSocket for a connection with the
			// given BluetoothDevice
			try
			{
				tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
			} catch (Exception e)
			{
				Log.e(TAG, "create() failed", e);
			}
			mmSocket = tmp;
		}

		public void run()
		{
			Log.i(TAG, "BEGIN mConnectThread");
			setName("ConnectThread");

			// Always cancel discovery because it will slow down a connection
			mAdapter.cancelDiscovery();

			// Make a connection to the BluetoothSocket
			try
			{
				// This is a blocking call and will only return on a
				// successful connection or an exception
				mmSocket.connect();
			} catch (Exception e)
			{
				// Close the socket
				try
				{
					mmSocket.close();
				} catch (Exception e2)
				{
					Log.e(TAG,
							"unable to close() socket during connection failure",
							e2);
				}
				connectionFailed();
				return;
			}

			// Reset the ConnectThread because we're done
			synchronized (BluetoothService.this)
			{
				mConnectThread = null;
			}

			// Start the connected thread
			connected(mmSocket, mmDevice);
		}

		public void cancel()
		{
			try
			{
				mmSocket.close();
			} catch (Exception e)
			{
				Log.e(TAG, "close() of connect socket failed", e);
			}
		}
	}

	/**
	 * This thread runs during a connection with a remote device. It handles all
	 * incoming and outgoing transmissions.
	 */
	private class ConnectedThread extends Thread
	{
		private final BluetoothSocket mmSocket;
		private final InputStream mmInStream;
		private final OutputStream mmOutStream;

		public ConnectedThread(BluetoothSocket socket)
		{
			Log.d(TAG, "create ConnectedThread");
			mmSocket = socket;
			InputStream tmpIn = null;
			OutputStream tmpOut = null;

			// Get the BluetoothSocket input and output streams
			try
			{
				tmpIn = socket.getInputStream();
				tmpOut = socket.getOutputStream();
			} catch (Exception e)
			{
				Log.e(TAG, "temp sockets not created", e);
			}

			mmInStream = tmpIn;
			mmOutStream = tmpOut;
		}

		public void run()
		{
			Log.i(TAG, "BEGIN mConnectedThread");
			byte[] buffer = new byte[BluetoothConstants.MAX_READ_LENGTH];
			int bytes;

			// Keep listening to the InputStream while connected
			while (true)
			{
				try
				{
					// Read from the InputStream
					bytes = mmInStream.read(buffer);

					Log.i(TAG, "@@@@@@@@@ -------get = " + bytes);

					if (bytes != 0)
					{
						// Send the obtained bytes to the UI Activity
						Log.i(TAG, "@@@@@@@@@ -------get = "
								+ new String(getStringValid(buffer)));
						mHandler.obtainMessage(BluetoothConstants.MESSAGE_READ,
								-1, -1, new String(getStringValid(buffer))).sendToTarget();
						buffer = new byte[BluetoothConstants.MAX_READ_LENGTH];
					}
				} catch (Exception e)
				{
					Log.e(TAG, "disconnected", e);
					connectionLost();
					break;
				}
			}
		}

		private String getStringValid(byte[] bytes)
		{
			String message = "";
			for (int i = 0; i < bytes.length; i++)
			{
				char ch = (char) bytes[i];
				if (ch > 32 && ch < 127)
				{
					message += ch;
				}
			}

			return message;
		}

		/**
		 * Write to the connected OutStream.
		 * 
		 * @param buffer
		 *            The bytes to write
		 */
		public void write(byte[] buffer)
		{
			try
			{
				mmOutStream.write(buffer);

				// Share the sent message back to the UI Activity
				mHandler.obtainMessage(BluetoothConstants.MESSAGE_WRITE, -1,
						-1, buffer).sendToTarget();
			} catch (Exception e)
			{
				Log.e(TAG, "Exception during write", e);
			}
		}

		public void cancel()
		{
			try
			{
				if (mmInStream != null)
				{
					mmInStream.close();
					Log.e(TAG, "Closing input stream...");
				}
				if (mmOutStream != null)
				{
					mmOutStream.close();
					Log.e(TAG, "Closing output stream...");
				}
			} catch (Exception ex)
			{
				ex.printStackTrace();
			}

			try
			{
				mmSocket.close();
				Log.e(TAG, "Closing Socket...");
			} catch (Exception e)
			{
				Log.e(TAG, "close() of connect socket failed", e);
			}
		}
	}
}
